top

Discord Botを作ろう③ 川柳BOTを作ろう

今日の目標!

今日作るもの

今日は、教室のみんなで言葉を登録して、ランダムに川柳を作るBotに挑戦する。

まずは、上五・中七・下五をそれぞれ登録する。

自分:!kamigo 古池や
Bot:「古池や」を上五に登録しました!

自分:!nakashichi 蛙飛びこむ
Bot:「蛙飛びこむ」を中七に登録しました!

自分:!shimogo 水の音
Bot:「水の音」を下五に登録しました!

いろいろな言葉を登録したあと、Botへ一句お願いする。

自分:!yonde

Bot:一句できました!

    古池や
      蛙飛びこむ
        水の音

どの言葉が選ばれるかはランダム。


final_image

たぶん、すごくいい一句ができることもあれば、意味不明な一句が爆誕することもある。むしろ、そっちが今日の本番かもしれない。

川柳Botの中で起きること

Botへ上五を登録すると、次のような流れで処理する。

「!kamigo 古池や」と投稿
        ↓
コマンドと「古池や」に分ける
        ↓
上五のリストへ追加する
        ↓
kami5.txtへ書き込む

「!yonde」が投稿されたら、3つのリストから1つずつ選ぶ。

上五リスト ──→ ランダムで1つ選ぶ
中七リスト ──→ ランダムで1つ選ぶ
下五リスト ──→ ランダムで1つ選ぶ
                       ↓
                  一句にして投稿

川柳の五・七・五について

みんなは「川柳」って知っているかな?

川柳は、身近な出来事や人の気持ちなどを、短い言葉で楽しく表したものだよ。

俳句と同じように、基本は「五・七・五」のリズムで作る。でも、俳句と違って、季節を表す「季語」は入れなくても大丈夫。

まずは、五・七・五の3つの部分の名前を見てみよう。

たとえば、こんな川柳を作ったとする。

リッキーは    ← 上五(5音)
きょうもとっても ← 中七(7音)
かっこいい   ← 下五(5音)

最初から順番に、上五・中七・下五になっているね。

この3つをつなげると、1つの川柳が完成!完成した1つの川柳を「一句」と呼ぶよ。

ただし、日本語の音の数え方は、文字数をlen()で数えるだけでは正しく判断できない。

たとえば、

きょう

は3文字だけど、「きょ・う」の2音として数える。

小さい「ゃ・ゅ・ょ」や「っ」、伸ばす音などもあるので、今回はBotによる音数の自動判定はしない。

登録する人が、上五は5音、中七は7音、下五は5音になっているか確認してね。

Botにも、まだできないことはあるのだ。

ファイルを準備しよう

今回のフォルダーは、次のような構成にする。


directory

ファイルはプログラムから自動で作るので、今の時点でdataフォルダーや3つのファイルがなくても大丈夫。

なぜファイルへ保存するの?


chicken

Pythonのリストに入れたデータは、Botが動いている間は覚えている。

kami_list = ["古池や", "五月雨を", "風吹けば"]

でも、Botを停止すると、リストの中身は消えてしまう。

リスト
Botを動かしている間だけ使える、一時的な記憶

そこで、登録された言葉をテキストファイルにも書き込む。

テキストファイル
Botを停止してもパソコンに残る記憶

Botを起動するときにファイルを読み直せば、前回登録した言葉を復活させられる!

テキストファイルに文字を保存してみよう

Botのコードを作る前に、Pythonからテキストファイルへ文字を保存する練習をしてみよう。

まず、bot.pyと同じフォルダーにsavetext.pyというファイルを作り、次のコードを入力する。

from pathlib import Path


phrase = input("保存したい言葉を入力してね:")

file_path = Path(__file__).parent / "saved_text.txt"

with open(file_path, "a", encoding="utf-8") as file:
    file.write(phrase + "\n")

print("saved_text.txtに保存しました!")

Path(__file__).parentは、「このsavetext.pyが置いてあるフォルダー」という意味。

そこへsaved_text.txtをつなげることで、ターミナルをどのフォルダーで開いていても、savetext.pyと同じ場所へ保存できる。

savetext.pyがあるフォルダー / saved_text.txt

ファイル名だけを"saved_text.txt"と書いた場合は、Pythonファイルではなく、ターミナルで現在開いているフォルダーが保存先になるので注意しよう。

コードを保存したら、ターミナルで実行しよう。

python savetext.py

好きな言葉を入力して、Enterキーを押してみよう。

保存したい言葉を入力してね:古池や
saved_text.txtに保存しました!

同じフォルダーにsaved_text.txtが作られるので、開いて中身を確認してみよう。

古池や

もう一度プログラムを実行して、別の言葉も入力してみよう。

古池や
五月雨を

前に保存した「古池や」が消えず、その下に「五月雨を」が追加されていれば成功!

これは、open()"a"が、ファイルの最後に文字を追加するモードだからだよ。

練習が終わったら、savetext.pysaved_text.txtは削除してOK。

このあと作る川柳Botでも、これと同じ方法で登録された言葉を保存する。

dataフォルダーの場所を決めよう

練習でも使ったPathを、bot.pyの一番上に追加する。

from pathlib import Path

Pathは、ファイルやフォルダーの場所を扱いやすくしてくれるPythonの機能。

次のコードで、bot.pyと同じ場所にあるdataフォルダーを指定する。

DATA_DIR = Path(__file__).parent / "data"
DATA_DIR.mkdir(exist_ok=True)

Path(__file__).parentは、「このbot.pyが置いてあるフォルダー」という意味。

そこへdataをつなげている。

bot.pyがあるフォルダー / data

mkdir()はフォルダーを作る命令。

exist_ok=Trueがあるので、すでにdataフォルダーがあってもエラーにならない。

3つのファイルを指定しよう

KAMI5_textfile = DATA_DIR / "kami5.txt"
NAKASHI7_textfile = DATA_DIR / "naka7.txt"
SHIMO5_textfile = DATA_DIR / "shimo5.txt"

KAMI5NAKASHI7SHIMO5で上五・中七・下五を表し、textfileでテキストファイルの場所だとわかる名前にしている。

テキストファイルへ保存しよう

ファイルへ言葉を追加する関数を用意する。

生徒用のコードでは関数の中身が歯抜けになっている。さっきのsavetext.pyを思い出しながら、ファイルへ書き込む処理を追加してみよう。

def save_phrase(file_path, phrase):
    with open(file_path, "a", encoding="utf-8") as file:
        file.write(phrase + "\n")

関数の中身

with open(file_path, "a", encoding="utf-8") as file:

open()は、Pythonからファイルを開く機能。

"a"のaは、append(追加する)のa。

次の行で、ファイルへ言葉を書き込む。

file.write(phrase + "\n")

\nは改行を表す特別な文字。

1つ登録するたびに改行することで、1行に1つの言葉を保存する。

古池や
五月雨を
風吹けば

テキストファイルから読み込もう

Botを起動するとき、保存されている言葉を読み込む関数も用意する。

これもまずはコピペでOK。

def load_phrases(file_path):
    if not file_path.exists():
        file_path.touch()
        return []

    with open(file_path, "r", encoding="utf-8") as file:
        return [line.strip() for line in file if line.strip()]

ファイルがまだ存在しない場合は、空のファイルを作って、空のリスト[]を返す。

if not file_path.exists():
    file_path.touch()
    return []

ファイルがある場合は、"r"、つまりread(読み込み)モードで開く。

with open(file_path, "r", encoding="utf-8") as file:

最後の行は少し難しいので、今は「ファイルの空でない行をリストにして返している」とわかればOK。

return [line.strip() for line in file if line.strip()]

ファイルの内容をリストへ入れよう

3つのファイルを読み込んで、それぞれのリストへ入れる。

kami_list = load_phrases(KAMI5_textfile)
naka_list = load_phrases(NAKASHI7_textfile)
shimo_list = load_phrases(SHIMO5_textfile)

初めて起動したときは、3つとも空のリストになる。

kami_list = []
naka_list = []
shimo_list = []

すでにファイルへ言葉が保存されていれば、その言葉が入ったリストになる。

上五登録コマンドを作ろう

前回と同じ方法で、コマンドと文章を分ける。

parts = text.split(maxsplit=1)
command = parts[0]

if len(parts) == 2:
    phrase = parts[1]
else:
    phrase = ""

今回は、登録する言葉をphraseという変数に入れる。

phraseは「ひとまとまりの言葉」という意味。

上五を登録する処理は、次のようになる。

if command == "!kamigo":
    if phrase == "":
        reply = "上五の言葉も入力してね!"
    elif "\n" in phrase:
        reply = "改行せず、1行で登録してね!"
    elif len(phrase) > 20:
        reply = "長すぎます!20文字以内で登録してね"
    else:
        kami_list.append(phrase)
        save_phrase(KAMI5_textfile, phrase)
        reply = f"「{phrase}」を上五に登録しました!"

特に大事なのは、この2行。

kami_list.append(phrase)
save_phrase(KAMI5_textfile, phrase)
リストへ追加する
        +
ファイルへ保存する

この2つを両方行うことで、今すぐ一句に使えて、Botを停止してもデータが残る。

中七と下五も登録しよう

上五を参考に、中七と下五の処理も作ろう。

変える場所は、主に次の3つ。

中七

elif command == "!nakashichi":
    if phrase == "":
        reply = "中七の言葉も入力してね!"
    elif "\n" in phrase:
        reply = "改行せず、1行で登録してね!"
    elif len(phrase) > 20:
        reply = "長すぎます!20文字以内で登録してね"
    else:
        naka_list.append(phrase)
        save_phrase(NAKASHI7_textfile, phrase)
        reply = f"「{phrase}」を中七に登録しました!"

下五

elif command == "!shimogo":
    if phrase == "":
        reply = "下五の言葉も入力してね!"
    elif "\n" in phrase:
        reply = "改行せず、1行で登録してね!"
    elif len(phrase) > 20:
        reply = "長すぎます!20文字以内で登録してね"
    else:
        shimo_list.append(phrase)
        save_phrase(SHIMO5_textfile, phrase)
        reply = f"「{phrase}」を下五に登録しました!"

一句詠んでもらおう

いよいよ、登録した言葉を組み合わせる。

elif command == "!yonde":

command"!yonde"と同じか確認している。最後にビックリマークを追加した!yonde!など、ほかの書き方には反応しない。

まずは、上五・中七・下五が1つ以上登録されているか確認する。

if not kami_list or not naka_list or not shimo_list:

どれか1つでも空なら、random.choice()で選べない。

その場合は、Botが足りないことを教える。

reply = "まだ言葉が足りません!上五・中七・下五を登録してね"

すべて登録されていれば、それぞれのリストから1つずつ選ぶ。

kami = random.choice(kami_list)
naka = random.choice(naka_list)
shimo = random.choice(shimo_list)

最後に、3行の文章へまとめる。

reply = f"""一句できました!

 {kami}
  {naka}
   {shimo}"""

全部つなげると、こうなる。

elif command == "!yonde":
    if not kami_list or not naka_list or not shimo_list:
        reply = "まだ言葉が足りません!上五・中七・下五を登録してね"
    else:
        kami = random.choice(kami_list)
        naka = random.choice(naka_list)
        shimo = random.choice(shimo_list)

        reply = f"""一句できました!

 {kami}
  {naka}
   {shimo}"""

登録数を確認できるようにしよう

何個くらい言葉が集まったか確認するコマンドもあると便利。

elif command == "!count":
    reply = f"""現在の登録数
上五:{len(kami_list)}個
中七:{len(naka_list)}個
下五:{len(shimo_list)}個"""

前回使ったlen()が、ここでも役立つ!

完成コード

bot.py全体は次のようになる。

ファイルの読み書きをする関数は、そのままコピペでOK。

今日特に注目するのは、リストへ追加する.append()、ファイルへ保存するsave_phrase()、ランダムに選ぶrandom.choice()だよ。

import os
import random
from pathlib import Path

import discord
from dotenv import load_dotenv


# .envファイルを読み込む
load_dotenv()
token = os.getenv("DISCORD_BOT_TOKEN")

# 保存用のdataフォルダーを用意する
DATA_DIR = Path(__file__).parent / "data"
DATA_DIR.mkdir(exist_ok=True)

# 上五・中七・下五を保存するファイル
KAMI5_textfile = DATA_DIR / "kami5.txt"
NAKASHI7_textfile = DATA_DIR / "naka7.txt"
SHIMO5_textfile = DATA_DIR / "shimo5.txt"


# テキストファイルから言葉を読み込む関数
def load_phrases(file_path):
    if not file_path.exists():
        file_path.touch()
        return []

    with open(file_path, "r", encoding="utf-8") as file:
        return [line.strip() for line in file if line.strip()]


# テキストファイルへ言葉を追加する関数
def save_phrase(file_path, phrase):
    with open(file_path, "a", encoding="utf-8") as file:
        file.write(phrase + "\n")


# 保存されている言葉をリストへ入れる
kami_list = load_phrases(KAMI5_textfile)
naka_list = load_phrases(NAKASHI7_textfile)
shimo_list = load_phrases(SHIMO5_textfile)

# Botが受け取りたいDiscordの情報を設定
intents = discord.Intents.default()
intents.message_content = True

# Discordに接続するBotを作る
client = discord.Client(
    intents=intents,
    allowed_mentions=discord.AllowedMentions.none(),
)


# Botのログインが完了したときに実行
@client.event
async def on_ready():
    print(f"{client.user} でログインしました!")
    print(
        f"上五:{len(kami_list)}個、"
        f"中七:{len(naka_list)}個、"
        f"下五:{len(shimo_list)}個を読み込みました"
    )


# Discordにメッセージが投稿されたときに実行
@client.event
async def on_message(message):
    # Botが送ったメッセージには反応しない
    if message.author.bot:
        return

    # Discordに投稿された文章を取り出す
    text = message.content.strip()

    # 「!」で始まらない普通の会話には反応しない
    if not text.startswith("!"):
        return

    # コマンドと、その後ろの言葉に分ける
    parts = text.split(maxsplit=1)
    command = parts[0]

    if len(parts) == 2:
        phrase = parts[1]
    else:
        phrase = ""

    # ヘルプを表示する
    if command == "!help":
        reply = """使えるコマンド
!kamigo 言葉:上五を登録する
!nakashichi 言葉:中七を登録する
!shimogo 言葉:下五を登録する
!yonde:ランダムに一句作る
!count:現在の登録数を表示する"""

    # 上五を登録する
    elif command == "!kamigo":
        if phrase == "":
            reply = "上五の言葉も入力してね!"
        elif "\n" in phrase:
            reply = "改行せず、1行で登録してね!"
        elif len(phrase) > 20:
            reply = "長すぎます!20文字以内で登録してね"
        else:
            kami_list.append(phrase)
            save_phrase(KAMI5_textfile, phrase)
            reply = f"「{phrase}」を上五に登録しました!"

    # 中七を登録する
    elif command == "!nakashichi":
        if phrase == "":
            reply = "中七の言葉も入力してね!"
        elif "\n" in phrase:
            reply = "改行せず、1行で登録してね!"
        elif len(phrase) > 20:
            reply = "長すぎます!20文字以内で登録してね"
        else:
            naka_list.append(phrase)
            save_phrase(NAKASHI7_textfile, phrase)
            reply = f"「{phrase}」を中七に登録しました!"

    # 下五を登録する
    elif command == "!shimogo":
        if phrase == "":
            reply = "下五の言葉も入力してね!"
        elif "\n" in phrase:
            reply = "改行せず、1行で登録してね!"
        elif len(phrase) > 20:
            reply = "長すぎます!20文字以内で登録してね"
        else:
            shimo_list.append(phrase)
            save_phrase(SHIMO5_textfile, phrase)
            reply = f"「{phrase}」を下五に登録しました!"

    # ランダムに一句作る
    elif command == "!yonde":
        if not kami_list or not naka_list or not shimo_list:
            reply = "まだ言葉が足りません!上五・中七・下五を登録してね"
        else:
            kami = random.choice(kami_list)
            naka = random.choice(naka_list)
            shimo = random.choice(shimo_list)

            reply = f"""一句できました!

 {kami}
  {naka}
   {shimo}"""

    # 登録数を表示する
    elif command == "!count":
        reply = f"""現在の登録数
上五:{len(kami_list)}個
中七:{len(naka_list)}個
下五:{len(shimo_list)}個"""

    # 知らないコマンドが入力された
    else:
        reply = "そのコマンドは知らないよ! !helpで確認してね"

    # 作った返事をDiscordへ送る
    await message.channel.send(reply)


# Tokenを使ってBotを起動
client.run(token)

Botを起動しよう

コードを保存したら、ターミナルでBotを起動する。

python bot.py

起動時に、保存されているデータの数が表示される。

○○Bot でログインしました!
上五:0個、中七:0個、下五:0個を読み込みました

まずは、3種類の言葉を1つずつ登録してみよう。

!kamigo 古池や
!nakashichi 蛙飛びこむ
!shimogo 水の音

登録できたら、お願いしてみよう。

!yonde

本当にデータが残るか実験しよう

テキストファイルへの保存が成功しているか、次の手順で確認する。

① 上五・中七・下五を登録する
        ↓
② dataフォルダーの3ファイルを開く
        ↓
③ 登録した言葉が書かれているか確認する
        ↓
④ Ctrl + CでBotを止める
        ↓
⑤ もう一度 python bot.py で起動する
        ↓
⑥ !countと!yondeを試す

再起動しても登録数が0に戻らず、前の言葉で一句作れたら成功!

みんなで川柳大会!

それぞれ、上五・中七・下五を1つ以上登録しよう。

登録するときの約束

言葉が集まったら、何度か!yondeを送ってみよう。

!yonde

一番おもしろかった一句や、一番きれいだった一句を選んで発表しよう!

自由改造タイム!

早く完成した人は、川柳Botに新しい機能を追加してみよう。

ミッションA:同じ言葉を登録させない

登録前に、すでにリストへ入っているか確認する。

if phrase in kami_list:
    reply = "その上五は、もう登録されているよ!"
else:
    kami_list.append(phrase)
    save_phrase(KAMI5_textfile, phrase)

ミッションB:川柳を飾り付ける

絵文字や記号を使って、完成した川柳を目立たせてみよう。

reply = f"""🌸 今日の一句 🌸

 {kami}
  {naka}
   {shimo}

      詠み人:川柳Bot"""

ミッションC:川柳に題名を付ける

題名のリストを用意して、題名もランダムに選んでみよう。

titles = ["放課後", "夏の思い出", "宿題の乱", "給食"]
title = random.choice(titles)

ミッションD:登録内容を表示する

上五の一覧を改行でつないで表示してみよう。

reply = "\n".join(kami_list)

登録数が多い場合は、Discordへ長すぎる文章を送らないように注意!

ミッションE:川柳をAIに講評してもらう

完成した川柳をAIに読んでもらい、良かったところや、おもしろかったところを講評してもらおう。

一句できました!

 朝起きて
  ふとんがぼくを
   はなさない

AIの講評:朝の眠さを、ふとんが引き止めているように表したところがおもしろい一句ですね!

ヒント

別のページで作ったGroq APIを使うBotを思い出してみよう。

▶ 補足 Discord BotからGroq APIを使ってみよう

まず、ランダムに選んだ上五・中七・下五を、1つの文字列にまとめる。

senryu = f"{kami}\n{naka}\n{shimo}"

次に、このsenryuをGroq APIへ送ってみよう。AIへ渡すお願いには、どんな講評をしてほしいかも書いておく。

prompt = f"""次の川柳を、小学生にもわかる言葉で短く講評してください。
良かったところや、おもしろかったところを1つ伝えてください。

{senryu}"""

Groqのページに出てきた、次の部分が使えそうだね。

APIキーをPythonコードへ直接書いたり、Discordへ投稿したりしないこと。AIの返事が来ないからといって、コマンドを連打するのもナシ!

AIの講評がいつも正しいとは限らない。最後は自分たちでも川柳を読んで、AIの講評に納得できるか考えてみよう。

発展:Discord Botは文字以外も扱える

ここからは、川柳Botが完成して時間が余った人向け。

Discord Botは、文字を送る以外にもいろいろなことができる。

リアクションを付ける

メッセージに拍手のリアクションを付けられる。

await message.add_reaction("👏")

川柳を詠んだときに付ければ、みんなでお気に入りの一句へ投票できる。

Embedでカード風に表示する

Embed(エンベッド)を使うと、タイトルや色が付いたカード風のメッセージを作れる。

embed = discord.Embed(
    title="今日の一句",
    description=f"{kami}\n{naka}\n{shimo}",
    color=0x57F287
)

await message.channel.send(embed=embed)

Embedを使う場合は、最後のsend(reply)とは別に送る必要がある。完成した人だけ、先生と一緒に改造しよう。

画像を投稿する

bot.pyと同じ場所にimgフォルダーを作り、senryu.svgを入れた場合は、次のコードで投稿できる。

image_path = Path(__file__).parent / "img" / "senryu.svg"
image_file = discord.File(str(image_path))

await message.channel.send(file=image_file)

画像の名前や場所が違うとエラーになるので注意。

投稿された画像を調べる

メッセージにファイルが添付されているかは、message.attachmentsで確認できる。

if message.attachments:
    attachment = message.attachments[0]
    await message.channel.send(
        f"ファイル名:{attachment.filename}"
    )

画像やファイルにも、ファイル名やURLなどの情報が入っている。

前回のmessage.contentが文章の情報だったように、message.attachmentsは添付ファイルの情報というわけだ!

うまく動かないとき

dataフォルダーが作られない

日本語が文字化けする

ファイルを開くコードに、次の指定があるか確認する。

encoding="utf-8"

登録したのに、再起動すると消える

kami_list.append(phrase)
save_phrase(KAMI5_textfile, phrase)

Cannot choose from an empty sequenceと表示される

空のリストにrandom.choice()を使うと出るエラー。

一句を作る前に、3つのリストを確認しているか見直そう。

if not kami_list or not naka_list or not shimo_list:

Botが同じ言葉をものすごい速さで送り続ける

Botの投稿を無視する処理が消えていないか確認する。

if message.author.bot:
    return

まずはターミナルでCtrl + Cを押して、Botを止めてから直そう。

今日のまとめ

最後のチャレンジ

自分たちの川柳Botについて、次の5つを説明してみよう。

  1. 上五を登録すると、どのリストとファイルに保存される?
  2. なぜリストだけでなく、テキストファイルにも保存する?
  3. Botを再起動したとき、どのようにデータを復元している?
  4. 一句を作るとき、random.choice()は何をしている?
  5. 川柳Botへもう1つ機能を追加するなら、何を作りたい?

全3回で、Discordのメッセージを受け取り、文字列を加工し、コマンドを判定し、データを保存して作品を作るところまでできた。

最初はただのオウム返しだったBotが、みんなの言葉を覚えて川柳を詠むようになった。これはもう、かなり立派なDiscord Botだ!